/**
 * Copyright (c) The openTCS Authors.
 * <p>
 * This program is free software and subject to the MIT license. (For details,
 * see the licensing information (LICENSE.txt) you should have received with
 * this copy of the software.)
 */
package org.opentcs.kernel.extensions.rmi;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

import static java.util.Objects.requireNonNull;

import java.util.Set;
import java.util.function.Predicate;
import javax.inject.Inject;

import org.opentcs.access.CredentialsException;
import org.opentcs.access.Kernel;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.access.LocalKernel;
import org.opentcs.access.rmi.ClientID;
import org.opentcs.access.rmi.factories.SocketFactoryProvider;
import org.opentcs.access.rmi.services.RegistrationName;
import org.opentcs.access.rmi.services.RemoteKernelServicePortal;
import org.opentcs.components.kernel.KernelExtension;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.kernel.extensions.rmi.UserManager.ClientEntry;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is the standard implementation of the {@link RemoteKernelServicePortal} interface.
 * <p>
 * Upon creation, an instance of this class registers itself with the RMI registry by the name
 * declared as {@link RemoteKernelServicePortal#getRegistrationName()}.
 * </p>
 *
 * @author Martin Grzenia (Fraunhofer IML)
 */
public class StandardRemoteKernelClientPortal implements RemoteKernelServicePortal, KernelExtension {

    /**
     * This class' logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(StandardRemoteKernelClientPortal.class);
    /**
     * The kernel.
     */
    private final Kernel kernel;
    /**
     * The kernel's remote services.
     */
    private final Set<KernelRemoteService> remoteServices;
    /**
     * The user manager.
     */
    private final UserManager userManager;
    /**
     * Provides configuration data.
     */
    private final RmiKernelInterfaceConfiguration configuration;
    /**
     * Provides socket factories used for RMI.
     */
    private final SocketFactoryProvider socketFactoryProvider;
    /**
     * Provides the registry with which this remote portal registers.
     */
    private final RegistryProvider registryProvider;
    /**
     * The event handler to publish events to.
     */
    private final EventHandler eventHandler;
    /**
     * The registry with which this remote portal registers.
     */
    private Registry rmiRegistry;
    /**
     * Whether this remote portal is initialized or not.
     */
    private boolean initialized;

    /**
     * Creates a new instance.
     *
     * @param kernel                The kernel.
     * @param remoteServices        The kernel's remote services.
     * @param userManager           The user manager.
     * @param configuration         This class' configuration.
     * @param socketFactoryProvider The socket factory provider used for RMI.
     * @param registryProvider      The provider for the registry with which this remote portal registers.
     * @param eventHandler          The event handler to publish events to.
     */
    @Inject
    public StandardRemoteKernelClientPortal(LocalKernel kernel,
                                            Set<KernelRemoteService> remoteServices,
                                            UserManager userManager,
                                            RmiKernelInterfaceConfiguration configuration,
                                            SocketFactoryProvider socketFactoryProvider,
                                            RegistryProvider registryProvider,
                                            @ApplicationEventBus EventHandler eventHandler) {
        this.kernel = requireNonNull(kernel, "kernel");
        this.remoteServices = requireNonNull(remoteServices, "remoteServices");
        this.userManager = requireNonNull(userManager, "userManager");
        this.configuration = requireNonNull(configuration, "configuration");
        this.socketFactoryProvider = requireNonNull(socketFactoryProvider, "socketFactoryProvider");
        this.registryProvider = requireNonNull(registryProvider, "registryProvider");
        this.eventHandler = requireNonNull(eventHandler, "eventHandler");
    }

    @Override
    public void initialize() {
        if (isInitialized()) {
            return;
        }

        registryProvider.initialize();
        userManager.initialize();

        rmiRegistry = registryProvider.get();
        // Export this instance via RMI.
        try {
            LOG.debug("Exporting proxy...");
            UnicastRemoteObject.exportObject(this, configuration.remoteKernelServicePortalPort(), socketFactoryProvider.getClientSocketFactory(), socketFactoryProvider.getServerSocketFactory());
            LOG.debug("Binding instance with RMI registry...");
            rmiRegistry.rebind(RegistrationName.REMOTE_KERNEL_CLIENT_PORTAL, this);
            LOG.debug("Bound instance {} with registry {}.", rmiRegistry.list(), rmiRegistry);
        } catch (RemoteException exc) {
            LOG.error("Could not export or bind with RMI registry", exc);
            return;
        }

        for (KernelRemoteService remoteService : remoteServices) {
            remoteService.initialize();
        }

        initialized = true;
    }

    @Override
    public boolean isInitialized() {
        return initialized;
    }

    @Override
    public void terminate() {
        if (!isInitialized()) {
            return;
        }

        for (KernelRemoteService remoteService : remoteServices) {
            remoteService.terminate();
        }

        try {
            LOG.debug("Unbinding from RMI registry...");
            rmiRegistry.unbind(RegistrationName.REMOTE_KERNEL_CLIENT_PORTAL);
            LOG.debug("Unexporting RMI interface...");
            UnicastRemoteObject.unexportObject(this, true);
        } catch (RemoteException | NotBoundException exc) {
            LOG.warn("Exception shutting down RMI interface", exc);
        }

        userManager.terminate();
        registryProvider.terminate();
        initialized = false;
    }

    @Override
    @SuppressWarnings("deprecation")
    public ClientID login(String userName, String password, Predicate<Object> eventFilter) throws CredentialsException {
        requireNonNull(userName, "userName");
        requireNonNull(password, "password");

        synchronized (userManager.getKnownClients()) {
            UserAccount account = userManager.getUser(userName);
            if (account == null || !account.getPassword().equals(password)) {
                LOG.debug("Authentication failed for user {}.", userName);
                throw new CredentialsException("Authentication failed for user " + userName);
            }

            // Generate a new ID for the client.
            ClientID clientId = new ClientID(userName);
            // Add an entry for the newly connected client.
            ClientEntry clientEntry = new ClientEntry(userName, account.getPermissions());
            clientEntry.getEventBuffer().setEventFilter(eventFilter);
            userManager.getKnownClients().put(clientId, clientEntry);
            LOG.debug("New client named {} logged in", clientId.getClientName());
            return clientId;
        }
    }

    @Override
    public void logout(ClientID clientID) {
        requireNonNull("clientID");

        // Forget the client so it won't be able to call methods on this kernel and won't receive
        // events any more.
        synchronized (userManager.getKnownClients()) {
            userManager.getKnownClients().remove(clientID);
            LOG.debug("Client named {} logged out", clientID.getClientName());
        }
    }

    @Override
    public Kernel.State getState(ClientID clientId) {
        userManager.verifyCredentials(clientId, UserPermission.READ_DATA);

        return kernel.getState();
    }

    @Override
    public List<Object> fetchEvents(ClientID clientId, long timeout)
            throws RemoteException {
        userManager.verifyCredentials(clientId, UserPermission.READ_DATA);

        return userManager.pollEvents(clientId, timeout);
    }

    @Override
    public void publishEvent(ClientID clientId, Object event) throws KernelRuntimeException {
        userManager.verifyCredentials(clientId, UserPermission.PUBLISH_MESSAGES);

        eventHandler.onEvent(event);
    }
}
